﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System.Threading.Actors
{
    // Provides a dynamic "projection" of an IActorState object, so
    // that we can pass this projection to actor methods that accept
    // a dynamic actor state.
    public class DynamicActorStateWrapper : DynamicObject
    {
        // The IActorState being "projected"
        IActorState m_state;

        // Constructor; accepts an IActorState
        public DynamicActorStateWrapper(IActorState state)
        {
            m_state = state;
        }

        // Allows for "int x = (int) state.SomeProperty;"
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            return m_state.TryGet(binder.Name, out result);
        }

        // Allows for "state.SomeProperty = x;"
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            m_state.Set(binder.Name, value);
            return true;
        }

        // Allows for 'state["SomeProperty"] = x;'
        public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
        {
            if (indexes == null || indexes.Length != 1) return false;
            var keyName = indexes[0] as string;
            if (keyName == null) return false;
            m_state.Set(keyName, value);
            return true;
        }

        // Allows for 'int x = (int) state["SomeProperty"];'
        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            result = null;
            if (indexes == null || indexes.Length != 1) return false;
            var keyName = indexes[0] as string;
            return m_state.TryGet(keyName, out result);
        }

        // Allows for "state.SomeActorMethod(a,b,c);"
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var methodName = binder.Name;
            result = null;

            // Check for special cases, i.e. reserved method names
            if (methodName.Equals("Remove"))
            {
                var keyToRemove = args[0] as string;
                m_state.Remove(keyToRemove);
                result = null;
                return true;
            }
            else if (methodName.Equals("Contains"))
            {
                var keyName = args[0] as string;
                object unused;
                result = m_state.TryGet(keyName, out unused);
                return true;
            }
            else if (methodName.Equals("ValueOrDefault"))
            {
                var keyName = args[0] as string;
                if (!m_state.TryGet(keyName, out result))
                {
                    Contract.Assume(args.Length > 1, "ValueOrDefault requires a parameter.  Dynamic should provide that.");
                    result = args[1];
                }
                return true;
            }
            else if (methodName.Equals("Publish"))
            {
                if (args.Length < 2) return false;
                var eventType = args[0] as string;
                var payload = args[1];
                if (args.Length > 2)
                {
                    var remainingArgs = args.Skip(2).ToArray();
                    m_state.Publish(eventType, payload, remainingArgs);
                }
                else m_state.Publish(eventType, payload);
                return true;
            }
            else if (methodName.Equals("GetActorProxy"))
            {
                // Allow the partition name to be an optional parameter
                if (args.Length < 1 || args.Length > 2) return false;
                var actorName = args[0] as string;
                var partitionName = (args.Length == 2) ? args[1] as string : null;
                var proxy = m_state.GetActorProxy(actorName, partitionName);
                result = new DynamicActorProxyWrapper(proxy);
                return true;
            }
            else if (methodName.Equals("CreateActor"))
            {
                if (args.Length != 2) return false;
                var actorName = args[0] as string;
                var creationString = args[1] as string;
                result = m_state.CreateActor(actorName, creationString);
                return true;
            }
            else if (methodName.Equals("DeleteActor"))
            {
                if (args.Length != 1) return false;
                var actorName = args[0] as string;
                m_state.DeleteActor(actorName);
                return true;
            }

            // It's not one of our reserved method names, so treat it as an actor method.
            result = m_state.CallMethod(methodName, args);
            return true;
        }

        // Allow conversion to non-dynamic IActorState
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if ((binder.Explicit && (binder.Type == typeof(IActorState))) ||
                  binder.ReturnType == typeof(IActorState))
            {
                result = m_state;
                return true;
            }

            return base.TryConvert(binder, out result);
        }

    }

    // Provides a dynamic projection of IActorProxy
    public class DynamicActorProxyWrapper : DynamicObject
    {
        private IActorProxy m_proxy;

        public DynamicActorProxyWrapper(IActorProxy proxy)
        {
            m_proxy = proxy;
        }

        // Allow for Request, Command and direct actor method calls.  
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var methodName = binder.Name;

            if (methodName.Equals("Request"))
            {
                var innerMethodName = (string)args[0];
                var innerArgs = args.Skip(1).ToArray();
                var innerPromise = m_proxy.Request(innerMethodName, innerArgs);
                result = new DynamicActorPromiseWrapper(innerPromise);
                return true;
            }
            else if (methodName.Equals("Command"))
            {
                var innerMethodName = (string)args[0];
                var innerArgs = args.Skip(1).ToArray();
                m_proxy.Command(innerMethodName, innerArgs);
                result = null;
                return true;
            }

            // Must be a direct method call.  This will be of the "returns a promise" flavor,
            // as opposed to the "fire-and-forget" flavor.
            var promise = m_proxy.Request(methodName, args);
            result = new DynamicActorPromiseWrapper(promise);
            return true;
        }

        // Normal index-based observable mechanism associated with IActorProxy
        public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
        {
            result = null;
            if (indexes == null || indexes.Length != 1) return false;
            var eventType = indexes[0] as string;
            result = new DynamicActorStateObservableWrapper(m_proxy[eventType]);
            return true;
        }

        // Allow conversion to static IActorProxy object
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if ((binder.Explicit && (binder.Type == typeof(IActorProxy))) ||
                  binder.ReturnType == typeof(IActorProxy))
            {
                result = m_proxy;
                return true;
            }

            return base.TryConvert(binder, out result);
        }
    }

    // Provides a dynamic projection of IActorPromise
    public class DynamicActorPromiseWrapper : DynamicObject
    {
        private IActorPromise m_promise;

        public DynamicActorPromiseWrapper(IActorPromise promise)
        {
            m_promise = promise;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var methodName = binder.Name;
            result = null;
            if (methodName.Equals("ContinueWith"))
            {
                if (args == null || args.Length != 1)
                    throw new ArgumentException("Expected a single argument to ContinueWith");
                result = m_promise.ContinueWith((string)args[0]);
                return true;
            }
            else if (methodName.Equals("GetTypedResult"))
            {
                // Crazy!  There is no easy way to grab the generic arguments from a dynamic call, but
                // evidently this works.  See
                // http://stackoverflow.com/questions/5492373/get-generic-type-of-call-to-method-in-dynamic-object
                var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
                var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>);
                if (typeArgs.Count != 1) throw new InvalidOperationException("Expected GetTypedResult to have a single generic parameter");
                
                Type t = typeArgs[0];
                var promiseType = typeof(ActorPromise);
                var method = promiseType.GetMethod("GetTypedResult");
                var gmethod = method.MakeGenericMethod(t);
                result = gmethod.Invoke(m_promise, null);
                return true;
            }

            return base.TryInvokeMember(binder, args, out result);
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var memberName = binder.Name;
            if (memberName.Equals("Result"))
            {
                result = m_promise.Result;
                return true;
            }
            else if (memberName.Equals("IsFaulted"))
            {
                result = m_promise.IsFaulted;
                return true;
            }
            else if (memberName.Equals("ExceptionType"))
            {
                result = m_promise.ExceptionType;
                return true;
            }
            else if (memberName.Equals("ExceptionMessage"))
            {
                result = m_promise.ExceptionMessage;
                return true;
            }
            else if (memberName.Equals("ExceptionStackTrace"))
            {
                result = m_promise.ExceptionStackTrace;
                return true;
            }

            return base.TryGetMember(binder, out result);
        }

        // We have two Wait overloads, and can't easily use dynamic method binding to call these.  However, putting the 
        // methods here works great.
        public void Wait()
        {
            m_promise.Wait();
        }

        public bool Wait(int timeoutMilliseconds)
        {
            return m_promise.Wait(timeoutMilliseconds);
        }

        // Allow conversion to static IActorPromise
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if ((binder.Explicit && (binder.Type == typeof(IActorPromise))) ||
                  binder.ReturnType == typeof(IActorPromise))
            {
                result = m_promise;
                return true;
            }

            return base.TryConvert(binder, out result);
        }
    }

    // Provides a dynamic projection of IActorStateObservable
    public class DynamicActorStateObservableWrapper : DynamicObject
    {
        private IActorStateObservable m_observable;

        public DynamicActorStateObservableWrapper(IActorStateObservable observable)
        {
            m_observable = observable;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var methodName = binder.Name;
            if (methodName.Equals("Subscribe"))
            {
                if (args == null || args.Length != 1)
                    throw new ArgumentException("Expected a single argument to Subscribe");
                result = m_observable.Subscribe((string)args[0]);
                return true;
            }
            else if (methodName.Equals("Unsubscribe"))
            {
                if (args == null || args.Length != 1)
                    throw new ArgumentException("Expected a single argument to Unsubscribe");
                m_observable.Unsubscribe((string)args[0]);
                result = null;
                return true;
            }

            return base.TryInvokeMember(binder, args, out result);
        }


        // Allow conversion to static IActorStateObservable
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if ((binder.Explicit && (binder.Type == typeof(IActorStateObservable))) ||
                  binder.ReturnType == typeof(IActorStateObservable))
            {
                result = m_observable;
                return true;
            }

            return base.TryConvert(binder, out result);
        }
    }

}
